/*
 * Copyright (C) 2007  Vianney le Clément
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package fsahoraires.store;

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.xml.sax.*;

import fsahoraires.programmes.Programme;
import fsahoraires.programmes.Semestre;
import fsahoraires.store.importers.*;

public class StoreManager {
	/**
	 * Map des importateurs. La clé est l'url complet du DTD.
	 */
	private static final Map<String, XMLImporter> importers;
	/**
	 * L'URL complet du DTD actuel (càd le seul qui ne sera pas marqué comme
	 * legacy)
	 */
	private static final String CURRENT_DTD;

	private static final DocumentBuilder builder;
	private static final Transformer transformer;

	private static final Preferences prefs = Preferences
			.userNodeForPackage(StoreManager.class);
	private static final String PREFS_KEY = "last-opened";

	static {
		// Initialiser les différents DTD supportés
		importers = new HashMap<String, XMLImporter>();
		importers
				.put(
						"http://fsa-horaires.googlecode.com/svn/web/dtd/fsahoraires-0.1.dtd",
						new XMLImporter01());
		importers
				.put(
						"http://fsa-horaires.googlecode.com/svn/web/dtd/fsahoraires-0.1.1.dtd",
						new XMLImporter011());
		importers
		.put(
				"http://fsa-horaires.googlecode.com/svn/web/dtd/fsahoraires-0.1.4.dtd",
				new XMLImporter014());
		CURRENT_DTD = "http://fsa-horaires.googlecode.com/svn/web/dtd/fsahoraires-0.1.4.dtd";

		// Créer le parseur xml
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setValidating(true);
		DocumentBuilder b = null;
		try {
			b = dbf.newDocumentBuilder();
			Callbacks cb = new Callbacks();
			b.setEntityResolver(cb);
			b.setErrorHandler(cb);
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		builder = b;

		// Créer le transformateur xml pour écriture
		TransformerFactory tf = TransformerFactory.newInstance();
		Transformer t = null;
		try {
			t = tf.newTransformer();
			t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, CURRENT_DTD);
			t.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
			t.setOutputProperty(OutputKeys.INDENT, "yes");
			t.setOutputProperty(OutputKeys.METHOD, "xml");
			t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
		} catch (TransformerConfigurationException e) {
			e.printStackTrace();
		}
		transformer = t;
	}

	public static File getLastOpened() {
		String file = prefs.get(PREFS_KEY, null);
		if (file == null)
			return null;
		return new File(file);
	}

	private static void setLastOpened(File file) {
		prefs.put(PREFS_KEY, file.getAbsolutePath());
	}

	/**
	 * Charge un fichier
	 * 
	 * @param file
	 * @return le résultat (programme + legacy) ou <code>null</code> si
	 *         l'ouverture a été impossible
	 */
	public static LoadResult load(File file) {
		if (builder == null)
			throw new IllegalStateException("XML parser not initialised");
		Document doc = null;
		try {
			doc = builder.parse(file);
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (doc != null) {
			// document XML
			DocumentType doctype = doc.getDoctype();
			if (doctype == null)
				return null;
			String dtd = doctype.getSystemId();
			XMLImporter importer = importers.get(dtd);
			if (importer == null)
				return null;
			Programme prog = importer.load(doc);
			if (prog == null)
				return null;
			setLastOpened(file);
			return new LoadResult(prog, !CURRENT_DTD.equals(dtd));
		} else {
			// legacy raw ASCII
			Programme prog = loadLegacyRaw(file);
			if (prog == null)
				return null;
			setLastOpened(file);
			return new LoadResult(prog, true);
		}
	}

	/**
	 * Enregistre le programme dans un fichier
	 * 
	 * @param programme
	 * @param file
	 * @return <code>true</code> si tout s'est passé correctement,
	 *         <code>false</code> en cas d'erreur
	 */
	public static boolean save(Programme programme, File file) {
		Document doc = builder.newDocument();
		try {
			CurrentExporter.save(programme, doc);
			transformer.transform(new DOMSource(doc), new StreamResult(file));
			setLastOpened(file);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	private static final Pattern FILE_SEMESTRE_PATTERN = Pattern
			.compile("/([0-9]+)/(1|2)");

	/**
	 * Crée un programme basé sur un ancien fichier de sauvegarde RAW ASCII
	 * utilisé dans les versions antérieures à 0.1. Ce fichier est lu ligne par
	 * ligne. Les lignes vides sont ignorées. Les espaces en début et fin de
	 * ligne sont ignorés.
	 * <p>
	 * Une ligne commençant par un slash ('/') indique le début d'un nouveau
	 * semestre. Le format est
	 * 
	 * <pre>
	 * /{ANNEE}/{SEMESTRE}
	 * </pre>
	 * 
	 * où <code>{ANNEE}</code> est un entier désignant l'année (1, 2, 3,...)
	 * et <code>{SEMESTRE}</code> est un entier désignant le semestre (1 ou
	 * 2). Un fichier de sauvegarde doit commencer par une ligne de ce type.<br>
	 * Exemple:
	 * 
	 * <pre>
	 * /2/1
	 * </pre>
	 * 
	 * désigne le premier semestre de la deuxième année.
	 * <p>
	 * Les autres lignes sont interprêtées comme étant des codes de cours à
	 * ajouter au semestre défini juste au-dessus.
	 * 
	 * @param file
	 * @return le programme chargé ou <code>null</code> si erreur
	 */
	private static Programme loadLegacyRaw(File file) {
		Programme prog = new Programme();
		BufferedReader buf = null;
		try {
			buf = new BufferedReader(new FileReader(file));
			Semestre current = null;
			String line;
			while ((line = buf.readLine()) != null) {
				line = line.trim();
				if (line.length() == 0)
					continue;
				if (line.startsWith("/")) {
					Matcher m = FILE_SEMESTRE_PATTERN.matcher(line);
					if (!m.matches())
						throw new IOException();
					int annee = Integer.parseInt(m.group(1));
					if (annee < 1)
						throw new IOException();
					int semestre = Integer.parseInt(m.group(2));
					current = new Semestre(prog, annee, semestre);
				} else {
					if (current == null)
						throw new IOException();
					current.add(prog.getManager().getCours(line));
				}
			}
			return prog;
		} catch (IOException e) {
			prog.close();
			return null;
		} finally {
			if (buf != null) {
				try {
					buf.close();
				} catch (IOException e) {
				}
			}
		}
	}

	public static class LoadResult {
		private final boolean legacy;
		private final Programme programme;

		public LoadResult(Programme programme, boolean legacy) {
			this.legacy = legacy;
			this.programme = programme;
		}

		public boolean isLegacy() {
			return legacy;
		}

		public Programme getProgramme() {
			return programme;
		}
	}

	private static class Callbacks implements EntityResolver, ErrorHandler {

		public InputSource resolveEntity(String publicId, String systemId)
				throws SAXException, IOException {
			XMLImporter importer = importers.get(systemId);
			if (importer == null)
				throw new SAXException("Unsupported DTD");
			InputStream stream = StoreManager.class.getResourceAsStream("dtd/"
					.concat(importer.getDTD()));
			if (stream == null)
				throw new SAXException("Unsupported DTD");
			return new InputSource(stream);
		}

		public void error(SAXParseException exception) throws SAXException {
			throw exception;
		}

		public void fatalError(SAXParseException exception) throws SAXException {
			throw exception;
		}

		public void warning(SAXParseException exception) throws SAXException {
			throw exception;
		}

	}

}
